package org.hive.testserver.domain.model.schedule;

import java.util.ArrayList;
import java.util.List;

import org.hive.testserver.domain.model.Slave;
import org.hive.testserver.domain.model.Test;
import org.hive.testserver.domain.model.TestRun;
import org.hive.testserver.domain.model.TestState;
import org.hive.testserver.domain.model.Work;
import org.hive.testserver.domain.model.WorkSheet;

/**
 * Rerun failed and aborted tests up to a maximum number of times. Tests are allocated in their natural order one by one. Tests to
 * be rerun are allocated to new slaves if possible, however if there is no other work for the slave they can be rerun on the same
 * slave.
 */
public class FixedRecoveryPolicy implements TestPolicy {

	public static final int MAXIMUM_RECOVERY_ATTEMPTS = 1;
	
	private int maximumRecoveryAttempts;

	public FixedRecoveryPolicy() {
		this(MAXIMUM_RECOVERY_ATTEMPTS);
	}
	
	public FixedRecoveryPolicy(int maximumRecoveryAttempts) {
		super();
		this.maximumRecoveryAttempts = maximumRecoveryAttempts;
	}

	public ScheduledTest nextTestOrNull(TestRun testRun, Slave slave, WorkSheet workSheet) {
		Test test = null;
		List<Test> potentialTests = remainingTests(testRun, workSheet);
		List<Test> preferOtherSlaveToRun = new ArrayList<Test>();
		while (test == null && !potentialTests.isEmpty()) {
			Test potentialTest = potentialTests.get(0);
			if (testHasAlreadyBeenExecutedBySlave(potentialTest, slave, workSheet)) {
				potentialTests.remove(0);
				preferOtherSlaveToRun.add(potentialTest);
			} else {
				test = potentialTest;
			}
		}
		//CL: Only re-run a test on this slave if:
		//    1. there are no tests left that have not been run on this slave ever.
		//    2. there are tests that have previously failed on this box that have not been finished (AKA rerun on another slave)
		//    3. there are no other slaves that are active (ie. Idle or running tests). 
		// If so then try once more on this slave.
		if (test == null && !preferOtherSlaveToRun.isEmpty() && !workSheet.areThereAnyOtherActiveSlaves(slave)) {
			test = preferOtherSlaveToRun.get(0);
		}
		if (test != null) {
			return new ScheduledTest(test, ScheduleReason.UNTESTED);
		} else {
			return new ScheduledTest(ScheduleReason.NOTHING_SUITABLE_TO_RUN);
		}
	}

	private boolean testHasAlreadyBeenExecutedBySlave(Test test, Slave slave, WorkSheet workSheet) {
		if (test.timesExecuted() == 0) {
			return false;
		}
		List<Work> works = workSheet.findWorksForTest(test);
		for (Work work : works) {
			if (slave.equals(work.assignedSlave())) {
				return true;
			}
		}
		return false;
	}

	//FIXME expensive - just temporary until some scoring mechanism with caching is in place
	List<Test> remainingTests(TestRun testRun, WorkSheet workSheet) {
		List<Test> remaining = new ArrayList<Test>();
		for (Test test : testRun.getTests()) {
			if (shouldConsiderRunning(test, workSheet)) {
				remaining.add(test);
			}
		}
		return remaining;
	}

	boolean shouldConsiderRunning(Test test, WorkSheet workSheet) {
		return untested(test) || problemTest(test) || rescheduled(test, workSheet);
	}

	private boolean rescheduled(Test test, WorkSheet workSheet) {
		return workSheet.isTestRescheduled(test);
	}

	private boolean untested(Test test) {
		return test.currentState().equals(TestState.UNTESTED);
	}

	private boolean problemTest(Test test) {
		return (test.currentState() == TestState.ABORTED||test.currentState() == TestState.FAILED) && test.timesExecuted() <= maximumRecoveryAttempts;
	}

	public void onRunStarted(TestRun testRun) {
		// do nothing
	}

	public void onTestStateUpdated(Test test) {
		if (shouldConsiderTestFinished(test)) {
			test.beFinished();
		}
	}

	private boolean shouldConsiderTestFinished(Test test) {
		return test.currentState() == TestState.PASSED || 
				((test.currentState() == TestState.ABORTED||test.currentState() == TestState.FAILED) && test.timesExecuted() > maximumRecoveryAttempts);
	}
}
